"""WMS接口"""


import logging
from odoo import http
import json
import re
import math
import datetime
import hashlib
import dateutil
import copy
from urllib.parse import quote, unquote
from base64 import b64encode, b64decode
from functools import wraps, update_wrapper

from odoo.http import request, Response, JsonRequest
from odoo import models, api, fields
from odoo.exceptions import UserError, ValidationError
from odoo.tools import date_utils
from odoo import api, SUPERUSER_ID
from odoo.tools import float_compare

from ..models.wms_base import DateDecoder, DateEncoder, DATE_FORMAT


"""验证机制,每次请求之前,先请求/web/session/authenticate获取session_id,并将此以参数形式回发"""

ERROR_INFO = {
    # 'timestamp': '缺少timestamp参数',
    'apptoken': '缺少token参数',
    'data': '缺少data参数',
    'sign': '缺少sign参数',
}

TIMEOUT_SECONDS = 5 * 60

_logger = logging.getLogger(__name__)


origin_json_response = JsonRequest._json_response

def _wms_json_response(self, result=None, error=None):
    """ODOO本身返回了多余的参数,去掉!"""
    if isinstance(result, dict) and 'returnFlag' in result and 'returnDesc' in result:
        mime = 'application/json'
        body = json.dumps(result, default=date_utils.json_default)

        return Response(
            body, status=error and error.pop('http_status', 200) or 200,
            headers=[('Content-Type', mime), ('Content-Length', len(body))]
        )
    else:
        return origin_json_response(self, result, error)

wrapped_json_response = update_wrapper(_wms_json_response, origin_json_response)
JsonRequest._json_response = wrapped_json_response


class WMSAPI(http.Controller):

    def pre_process(self, post):
        """检查数据完整性,验签"""
        self._check_post(post)
        user_id = http.request.env['ir.config_parameter'].sudo().get_param('nd.wms.api.odoo_user_id', default=False)
        if not user_id:
            user_id = SUPERUSER_ID
        http.request.custom_env = http.request.env['res.users'].sudo(user=user_id).with_context(lang='zh_CN')

    @staticmethod
    def return_json_response(msg="操作成功!", err=False):
        """以json形式返回信息, 0 成功 1 失败"""
        data = {
            'returnFlag': err and 1 or 0,
            'returnDesc': str(msg)
        }
        return data

    def _check_post(self, post):
        """检查参数"""
        for key, err in ERROR_INFO.items():
            if key not in post or post[key] is False:
                raise UserError(err)

        # if abs(self._to_datetime(post['timestamp']) - fields.Datetime.now()).seconds > TIMEOUT_SECONDS:
        #     raise UserError('请求超时！')

        if post['apptoken'] != http.request.env['ir.config_parameter'].sudo().get_param('nd.wms.api.odoo_token', default=''):
            raise UserError('apptoken不正确！')

        if not self._check_sign(post):
            raise UserError('验签未通过！')

    @staticmethod
    def _check_sign(post):
        """验签"""
        if 'sign' in post:
            sign = post.pop('sign')
            data = post
            odoo_secret = http.request.env['ir.config_parameter'].sudo().get_param('nd.wms.api.odoo_secret', default='')
            serialized_data = json.dumps(data, cls=DateEncoder, sort_keys=True, separators=(',', ':'), ensure_ascii=False)
            hl = hashlib.md5()
            encoded_data = (odoo_secret + serialized_data + odoo_secret).encode('utf-8')
            hl.update(encoded_data)
            hashed_data = hl.hexdigest().upper()
            return hashed_data == sign
        return False

    @staticmethod
    def _to_number(str_num, _float=False):
        """
        将字符串转成数字.
        :param str_num: str,待转的字符
        :param _float: bool,是否转成float,否则转陈int
        :return: float|int
        """
        try:
            if _float:
                return float(str_num)
            else:
                return int(str_num)
        except ValueError:
            raise UserError(f'"{str_num}"不能被转换成数字.')

    @staticmethod
    def _to_datetime(str_date):
        """
        将string形式的时间,转化曾python时间日期对象
        :return: datetime
        """
        if bool(str_date):
            if re.match('^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}$', str_date):
                return datetime.datetime.strptime(str_date, DATE_FORMAT)
            else:
                return dateutil.parser.parse(str_date)
        return False

    def _get_available_picking(self, picking_id):
        """
        根据指定的调拨单ID,寻找待完成的调拨单.
        主要原因在于欠单逻辑,如果WMS回传的pickingID在ODOO里未完成,则直接返回此picking,
        如果ODOO里已经完成了,则认为此次回传是完成的欠单,所以需要返回相应的欠单
        :param picking_id: int|str, 调拨单ID
        :return: stock.picking
        """
        env = http.request.custom_env.env
        picking = env['stock.picking'].browse(self._to_number(picking_id))
        finished_states = ['done', 'cancel']

        if not picking:
            raise UserError(f'没有找到ID为"{picking_id}"的出入库单')

        if picking.state not in finished_states:
            return picking
        else:
            backorder_ids = picking.backorder_ids
            while backorder_ids:
                unfinished_backorder_id = backorder_ids.filtered(lambda x: x.state not in finished_states)
                if unfinished_backorder_id:
                    return unfinished_backorder_id
                else:
                    backorder_ids = backorder_ids.mapped('backorder_ids')
            raise UserError(f'ID为"{picking_id}"的出入库单已经完成,并且没有未完成的欠单存在!')

    def _locate_move_line(self, picking_id, product_code, line_id):
        """根据给定的line_id和product_code在picking_id里定位相应的调拨明细stock.move"""
        move = picking_id.move_lines.filtered(lambda x: x.id == self._to_number(line_id))
        if not move:
            move = picking_id.move_lines.filtered(lambda x: x.product_id.default_code == product_code)
        if not move:
            raise UserError(f'在ID为"{picking_id.id}"的出入库单没有找到产品"{product_code}"的明细行!')
        return move

    # def _locate_move_line_ids(self, picking_id, product_code, line_id):
    #     """根据给定的line_id和product_code在picking_id里定位相应的调拨明细stock.move.line"""
    #     move_line = picking_id.move_line_ids.filtered(lambda x: x.id == self._to_number(line_id))
    #     # if not move:
    #     #     move = picking_id.move_lines.filtered(lambda x: x.product_id.default_code == product_code)
    #     if not move_line:
    #         raise UserError(f'在ID为"{picking_id.id}"的出入库单没有找到产品"{product_code}"的明细行!')
    #     return move_line

    def _get_warehouse_id(self, warehouse_name, picking_or_scrap=None):
        """根据库房名称,返回库房, TODO 目前只支持单仓库，原因是评库、报废等操作中WMS未回传仓库代码，无从判断是哪个仓库"""
        env = http.request.custom_env.env
        location_id = env['stock.location'].search([('name', '=', warehouse_name)], order='id', limit=1)
        if not location_id:
            if picking_or_scrap and picking_or_scrap._name == 'stock.picking':
                parent_location = picking_or_scrap.picking_type_id.warehouse_id.view_location_id
            elif picking_or_scrap and picking_or_scrap._name == 'stock.scrap':
                parent_location = picking_or_scrap.location_id.location_id
            else:
                parent_location = self.env['stock.warehouse'].search([], limit=1).view_location_id
            location_id = env['stock.location'].create({'name': warehouse_name,
                                                        'location_id': parent_location.id,
                                                        'usage': 'internal'})
            # raise UserError(f'没有找到名为"{warehouse_name}"的库房位置')
        return location_id

    def _get_uom_id(self, uom_name):
        """根据单位名称获取单位"""
        env = http.request.custom_env.env
        uom_id = env['uom.uom'].search([('name', '=', uom_name)], order='id', limit=1)
        if not uom_id:
            raise UserError(f'没有找到名为"{uom_name}"的单位')
        return uom_id

    def _get_lot_id(self, lot_name, product_code):
        """根据批次号获取批次ID"""
        env = http.request.custom_env.env
        lot_id = env['stock.production.lot'].search([('name', '=', lot_name), ('product_id.default_code', '=', product_code)], order='id', limit=1)
        if not lot_id:
            raise UserError(f'没有找到产品编码为"{product_code}"名下编号为"{lot_name}"的批次!')
        return lot_id

    @staticmethod
    def _has_lot(product):
        """判断指定的产品是否批次管控"""
        return product.tracking == 'lot'

    @staticmethod
    def _get_product_id(product_code):
        """根据产品编码返回产品"""
        if not product_code:
            raise UserError('没有产品编码或者产品编码为空!')
        env = http.request.custom_env.env
        product_id = env['product.product'].search([('default_code', '=', product_code)], limit=1)
        if not product_id:
            raise UserError(f'没有找到产品编码为"{product_code}"的产品')
        return product_id

    @staticmethod
    def _check_necessary_data_fields(post_data, fields):
        """检查业务数据post_data里是否包含指定字段fields"""
        diff_fields = set(fields) - set(post_data.keys())
        if diff_fields:
            raise UserError(f'缺少字段: {", ".join(diff_fields)}')

    def json_response(msg):
        def wrapped(f):
            @wraps(f)
            def wrapper(self, *args, **kws):
                post_data = http.request.jsonrequest
                try:
                    with http.request.cr.savepoint():
                        f(self, *args, **kws)
                    _logger.info(f'{msg} 成功!\n请求数据:{post_data}')
                    return self.return_json_response()
                except UserError as e:
                    _logger.info(f'{msg} 失败!原因:{str(e)}\n请求数据:{post_data}')
                    return self.return_json_response(e, err=True)
                except Exception as e:
                    _logger.info(f'{msg} 失败!原因:{str(e)}\n请求数据:{post_data}')
                    return self.return_json_response(e, err=True)
            return wrapper
        return wrapped

    @http.route(['/api/in_stock/'], methods=['POST'], auth='public', csrf=False, type='json')
    @json_response("入库单确认")
    def nd_in_stock(self, *args, **kws):
        """入库确认 TODO 入库时间\有效期时间\各种字段检查
        data: {'picking_id':'入库单ID', 'wms_no': 'WMS系统单号',
        'order_type': 'PO, RT','JH'  TODO 需处理
            'picking_lines': [
                {'line_id': '明细行ID', 'product_code': '产品编码', 'date': '收货时间',
                # 有批次
                'lot_lines': [{'lot_no': '批次号', 'qty_done': '实收数量', 'uom': '单位',
                                'warehouse': '目的库房', 'production_date': '生产日期',
                                'expire_date': '过期日期'}]
                # 无批次
                'qty_done': '实收数量', 'uom': '单位',
                }
            ]}
            {'picking_id': 32, 'picking_lines': [
                {'line_id': 33, 'product_code': 'TTTTTTTT', 'date': '', 'lot_lines': [
                    {'lot_no': '555', 'qty_done': '1', 'uom': '件', 'warehouse': 'Stock',
                    'production_date': '', 'expire_date': ''}
                ]}
            ]}
        """
        post = http.request.jsonrequest     # 居然不能以参数形式获取?
        self.pre_process(post)
        env = http.request.custom_env.env

        post_data = post['data']
        self._check_necessary_data_fields(post_data, fields=['picking_id', 'picking_lines', 'order_type'])
        for line_data in post_data['picking_lines']:
            self._check_necessary_data_fields(line_data, fields=['line_id', 'product_code', 'lot_lines'])
            for lot_data in line_data.get('lot_lines', []):
                self._check_necessary_data_fields(lot_data, fields=['lot_no', 'qty_done', 'warehouse'])

        picking_id = self._get_available_picking(post_data['picking_id'])
        is_return = post_data['order_type'] == 'RT'
        # 先取消保留
        picking_id.do_unreserve()
        data_to_write = {'wms_no': post_data.get('wms_no', '')}

        for line in post_data['picking_lines']:
            move_id = self._locate_move_line(picking_id, line['product_code'], line['line_id'])
            move_data = {}

            if line.get('date'):
                move_data['date'] = line['date']

            # 要分别考虑有批次和无批次的情况
            move_line_ids = move_id.move_line_ids

            for i in range(len(line['lot_lines']) - len(move_line_ids)):
                move_line_ids |= env['stock.move.line'].create(move_id._prepare_move_line_vals())

            for lot_data, move_line in zip(line['lot_lines'], move_line_ids):
                data = {
                        # 'lot_name': lot_data.get('lot_no'),
                        'qty_done': lot_data['qty_done'],
                        }
                # 针对一个批次号可能重复入库
                Lot = env['stock.production.lot']
                if lot_data.get('lot_no'):
                    data.update({
                        'nd_production_date': self._to_datetime(lot_data['production_date']),
                        'nd_expire_date': self._to_datetime(lot_data['expire_date']),
                    })
                    lot_id = Lot.search([('product_id', '=', move_id.product_id.id), ('name', '=', lot_data['lot_no'])], limit=1)
                    if lot_id:
                        data['lot_id'] = lot_id.id
                    else:
                        lot_id = Lot.create({'product_id': move_id.product_id.id, 'name': lot_data['lot_no']})
                        data['lot_id'] = lot_id.id
                        # 对于一张单据上相同产品有2个相同批次号的如下方法将会出错
                        # data['lot_name'] = lot_data['lot_no']

                if is_return:
                    if move_line.product_id.tracking == 'lot':
                        data['lot_id'] = self._get_lot_id(lot_name=lot_data['lot_no'], product_code=move_line.product_id.default_code).id
                else:
                    data['lot_name'] = lot_data.get('lot_no')        # 对于没有批次管控的,此处也能满足
                if lot_data.get('warehouse'):
                    data['location_dest_id'] = self._get_warehouse_id(lot_data['warehouse'], picking_or_scrap=picking_id).id
                if lot_data.get('uom'):
                    data['product_uom_id'] = self._get_uom_id(lot_data['uom']).id
                move_line_datas = move_data.setdefault('move_line_ids', [])
                move_line_datas.append((1, move_line.id, data))
            move_datas = data_to_write.setdefault('move_lines', [])
            move_datas.append((1, move_id.id, move_data))

        picking_id.write(data_to_write)
        picking_id.action_done()
        return

    @http.route(['/api/out_stock/'], methods=['POST'], auth='public', csrf=False, type='json')
    @json_response("出库单确认")
    def nd_out_stock(self, *args, **kws):
        """出库确认 TODO 出库时间\各种字段检查
        data: {'picking_id':'入库单ID', 'wms_no': 'WMS系统单号',
            'order_type': ''
KS	跨组织出库
SO	销售出库    #
PT	采购退货    #
JS	寄售出库
CJ	抽送检发货
XH	销毁出库    #
NS	内部销售
QS	其它出库
            'picking_lines': [
                {'line_id': '明细行ID', 'product_code': '产品编码', 'date': '收货时间',
                'lot_lines': [{'lot_no': '批次号', 'qty_done': '实收数量', 'uom': '单位',
                                'warehouse': '目的库房', 'production_date': '生产日期',
                                'expire_date': '过期日期'}]
                }
            ]}
            {'picking_id': 32, 'picking_lines': [
                {'line_id': 33, 'product_code': 'TTTTTTTT', 'date': '', 'lot_lines': [
                    {'lot_no': '555', 'qty_done': '1', 'uom': '件', 'warehouse': 'Stock',
                    'production_date': '', 'expire_date': ''}
                ]}
            ]}
        """
        post = http.request.jsonrequest  # 居然不能以参数形式获取?
        self.pre_process(post)
        env = http.request.custom_env.env

        post_data = post['data']
        self._check_necessary_data_fields(post_data, fields=['picking_id', 'picking_lines', 'order_type'])
        for line_data in post_data['picking_lines']:
            self._check_necessary_data_fields(line_data, fields=['line_id', 'product_code', 'lot_lines'])
            for lot_data in line_data.get('lot_lines', []):
                self._check_necessary_data_fields(lot_data, fields=['qty_done', 'warehouse'])

        order_type = post_data['order_type']
        if order_type in ['SO', 'PT', 'JS']:
            picking_id = self._get_available_picking(post_data['picking_id'])
            self._confirm_out_picking(post_data, picking_id)
            return picking_id.action_done()
        elif order_type == 'XH':
            return self._confirm_scrap(post_data)
        else:
            raise UserError(f'不能识别的类别：{order_type}')

    def _confirm_scrap(self, post_data):
        """确认报废单"""
        env = http.request.custom_env.env
        try:
            scrap_id = int(post_data['picking_id'])
        except Exception:
            raise UserError(f'picking_id值“{post_data["picking_id"]}”必须是整数')
        scrap_id = env['stock.scrap'].browse(scrap_id)
        if not scrap_id.exists():
            raise UserError(f'没有找到ID为{scrap_id}的报废单')
        if scrap_id.state != 'draft':
            raise UserError(f'报废单不处于草稿状态，无法操作！')

        picking_lines = post_data['picking_lines'][0]
        lot_lines = picking_lines['lot_lines'][0]
        product_code = picking_lines['product_code']

        product_id = self._get_product_id(product_code)
        if product_id != scrap_id.product_id:
            raise UserError(f'ERP系统的报废产品是“{scrap_id.product_id.default_code}”，与WMS系统回传信息不一致！')
        data = {
            'wms_no': post_data.get('wms_no', ''),
            'scrap_qty': lot_lines['qty_done'],
            'uom': self._get_uom_id(lot_lines['uom']).id,
            # WMS里是从KZZ出库，而没有专门的不合格品库位
            # 'location_id': self._get_warehouse_id(lot_lines['warehouse'], picking_or_scrap=scrap_id).id
        }
        if self._has_lot(product_id):
            data['lot_id'] = self._get_lot_id(lot_name=lot_lines['lot_no'], product_code=product_code).id
        if picking_lines.get('date'):
            data['date_expected'] = picking_lines['date']

        scrap_id.write(data)
        precision = env['decimal.precision'].precision_get('Product Unit of Measure')
        available_qty = sum(env['stock.quant']._gather(scrap_id.product_id,
                                                        scrap_id.location_id,
                                                        scrap_id.lot_id,
                                                        scrap_id.package_id,
                                                        scrap_id.owner_id,
                                                        strict=True).mapped('quantity'))
        scrap_qty = scrap_id.product_uom_id._compute_quantity(scrap_id.scrap_qty, product_id.uom_id)
        if float_compare(available_qty, scrap_qty, precision_digits=precision) < 0:
            raise UserError(f'所选位置“{scrap_id.location_id.name}“上产品数量”{available_qty}'
                             f'{product_id.uom_id.name}“不足”{scrap_id.scrap_qty}{scrap_id.product_uom_id.name}“，不能报废！')
        else:
            res = scrap_id.action_validate()
            if isinstance(res, dict):
                raise UserError(f'所选位置“{scrap_id.location_id.name}“上产品数量”{available_qty}'
                                 f'{product_id.uom_id.name}“不足”{scrap_id.scrap_qty}{scrap_id.product_uom_id.name}“，不能报废！')

    def __combine_same_line(self, post_data):
        """将相同产品的明细行合并起来"""
        # picking_lines = copy.deepcopy(post_data['data']['picking_lines'])
        # new_lines = {}
        # for line in picking_lines:
        #     new_line_data_list = new_lines.setdefault(line['line_id'], [])
        #     new_line_data_list.extend(line['lot_lines'])

        new_picking_lines = []
        for line in post_data['picking_lines']:
            for new_line in new_picking_lines:
                if new_line['line_id'] == line['line_id']:
                    new_line['lot_lines'].extend(line['lot_lines'])
                    break
            else:
                new_picking_lines.append(copy.deepcopy(line))
        post_data['picking_lines'] = new_picking_lines
        return post_data

    def _confirm_out_picking(self, post_data, picking_id, need_location=True):
        """填充回传数据到调拨单明细里"""
        env = http.request.custom_env.env
        # 先取消保留
        # picking_id.do_unreserve()
        data_to_write = {'wms_no': post_data.get('wms_no', '')}

        # 先合并相同产品行
        post_data = self.__combine_same_line(post_data)
        already_filled = [] # 由于WMS回传格式不总是遵守相关文档，这里需要标记哪些stock.move.line已经被填充过了，防止重复填充

        for line in post_data['picking_lines']:
            # TODO FIX
            move_id = self._locate_move_line(picking_id, line['product_code'], line['line_id'])
            move_data = {}

            if line.get('date'):
                move_data['date'] = line['date']

            move_line_ids = move_id.move_line_ids

            for i in range(len(line['lot_lines']) - len(move_line_ids)):
                move_line_ids |= env['stock.move.line'].create(move_id._prepare_move_line_vals())

            for lot_data, move_line in zip(line['lot_lines'], move_line_ids.filtered(lambda x: x.id not in already_filled)):
                already_filled.append(move_line.id)
                data = {'qty_done': lot_data['qty_done']}

                # 要区分有批次和无批次
                if self._has_lot(move_id.product_id):
                    data['lot_id'] = self._get_lot_id(lot_name=lot_data['lot_no'], product_code=line['product_code']).id

                # 在更新寄售出库单是，不能修改库位
                if lot_data.get('warehouse') and need_location:
                    data['location_id'] = self._get_warehouse_id(lot_data['warehouse'], picking_or_scrap=picking_id).id
                if lot_data.get('uom'):
                    data['product_uom_id'] = self._get_uom_id(lot_data['uom']).id
                move_line_datas = move_data.setdefault('move_line_ids', [])
                move_line_datas.append((1, move_line.id, data))
                # move_line.write(data)
            move_datas = data_to_write.setdefault('move_lines', [])
            move_datas.append((1, move_id.id, move_data))

        picking_id.write(data_to_write)
        # picking_id.action_done()

    @http.route(['/api/inventory/'], methods=['POST'], auth='public', csrf=False, type='json')
    @json_response("库存调整单确认")
    def nd_inventory(self, *args, **kws):
        """库存调整确认 TODO 调整时间\各种字段检查
        data: {'product_code':'产品编码', 'date ': '调整日期', 'customer_id': '货主ID'
            'wms_no': 'wms单号', 'warehouse': '调整库房'
            'lot_lines': [{'lot_no': '批次号', 'real_qty': '调整后数量', 'uom': '单位'}]
            }
        """
        post = http.request.jsonrequest  # 居然不能以参数形式获取?
        self.pre_process(post)
        env = http.request.custom_env.env

        post_data = post['data']

        self._check_necessary_data_fields(post_data, fields=['product_code', 'customer_id', 'warehouse'])
        for lot_data in post_data.get('lot_lines', []):
            self._check_necessary_data_fields(lot_data, fields=['lot_no', 'real_qty', ])

        product_id = self._get_product_id(post_data.get('product_code'))
        wms_no = post_data.get('wms_no', False)
        location_id = self._get_warehouse_id(post_data['warehouse'])

        data = {
            'name': wms_no or f'{fields.Date.today()}-调整单',
            'location_id': location_id.id,
            'filter': 'product',
            'product_id': product_id.id,
            'wms_no': wms_no,
        }

        # TODO ,货主一定是本公司吗,是否可能是客户?
        company_id = self._get_company_base_customerid(post_data['customer_id'])
        if company_id:
            data['company_id'] = company_id.id

        inventory_id = env['stock.inventory'].create(data)
        inventory_id.action_start()
        datas = {'line_ids': []}
        datas_create = []

        inventory_info = env['stock.inventory.line'].search_read([('id', 'in', inventory_id.line_ids.ids)],
                                                                 fields=['product_id', 'prod_lot_id'])
        if self._has_lot(product_id):
            # 有批次控制的,根据传入的批次信息来更新对于调整明细行内容,对于没有找到对应调整明细的批次(也就是批次不存在)则新建
            inventory_info_dict = {invent_line['prod_lot_id'][1]: invent_line['id'] for invent_line in inventory_info}

            for line in post_data['lot_lines']:
                if line['lot_no'] not in inventory_info_dict:
                    # 没有批次的话,新建一行包含此批次的调整明细
                    lot_id = env['stock.production.lot'].create({'product_id': product_id.id, 'name': line['lot_no']})
                    data_to_create = {'inventory_id': inventory_id.id,
                                      'location_id': location_id.id,
                                      'product_id': product_id.id,
                                      'partner_id': inventory_id.partner_id.id,
                                      'product_qty': line['real_qty'],
                                      'product_uom_id': self._get_uom_id(line['uom']).id,
                                      'prod_lot_id': lot_id.id}
                    datas_create.append(data_to_create)
                else:
                    to_update_data = {'product_qty': line['real_qty']}
                    if line.get('uom'):
                        to_update_data['product_uom_id'] = self._get_uom_id(line['uom']).id
                    datas['line_ids'].append((1, inventory_info_dict[line['lot_no']], to_update_data))
        else:
            # 如果没有批次,则认为只有一行调整明细,将用传入的第一个lot_lines参数来更新第一个调整明细
            line = post_data['lot_lines'][0]
            to_update_data = {'product_qty': line['real_qty']}
            if line.get('uom'):
                to_update_data['product_uom_id'] = self._get_uom_id(line['uom']).id
            datas['line_ids'].append((1, inventory_id.line_ids[0].id, to_update_data))

        if datas_create:
            env['stock.inventory.line'].create(datas_create)
        inventory_id.write(datas)
        inventory_id.action_validate()
        return

    @http.route(['/api/internal_stock/'], methods=['POST'], auth='public', csrf=False, type='json')
    @json_response("内部调拨单确认")
    def nd_internal_stock(self, *args, **kws):
        """内部调拨单确认 TODO 调拨时间\各种字段检查
        data: {'product_code':'产品编码', 'date ': '调整日期', 'customer_id': '货主ID'
            'wms_no': 'wms单号', 'src_warehouse': '源仓库位置', 'dest_warehouse': '目的仓库位置',
            'is_scrap': "是否报废", 'notes': '备注',
            'lot_lines': [{'lot_no': '批次号', 'qty_done': '移动数量', 'uom': '单位'}]
            }
        """
        post = http.request.jsonrequest  # 居然不能以参数形式获取?
        self.pre_process(post)
        env = http.request.custom_env.env
        post_data = post['data']

        self._check_necessary_data_fields(post_data, fields=['product_code', 'customer_id', 'src_warehouse', 'dest_warehouse'])
        for lot_data in post_data.get('lot_lines', []):
            self._check_necessary_data_fields(lot_data, fields=['lot_no', 'qty_done'])

        # product_id = self._get_product_id(post_data.get('product_code'))
        # location_id = self._get_warehouse_id(post_data['src_warehouse'])

        # is_scrap = post_data.get('is_scrap', "0")
        # if is_scrap == "1":
        #     self._execute_scrap(post_data)
        # else:
        #     self._execute_internal_picking(post_data)
        self._execute_internal_picking(post_data)

    def _execute_scrap(self, post_data):
        """执行报废操作 TODO 暂时停用"""
        env = http.request.custom_env.env
        self._check_necessary_data_fields(post_data, fields=['product_code', 'customer_id', 'src_warehouse'])
        for lot_data in post_data.get('lot_lines', []):
            self._check_necessary_data_fields(lot_data, fields=['lot_no', 'qty_done'])

        product_id = self._get_product_id(post_data.get('product_code'))
        location_id = self._get_warehouse_id(post_data['src_warehouse'])
        scrap_location_id = env.ref('stock.stock_location_scrapped')
        if not scrap_location_id:
            scrap_location_id = env['stock.picking'].search([('scrap_location', '=', True)], limit=1)
        if not scrap_location_id:
            raise UserError('没有找到报废库位,请先配置一个报废库位!')

        datas = []
        for lot_data in post_data['lot_lines']:
            data = {
                'product_id': product_id.id,
                'scrap_location_id': scrap_location_id.id,
                'scrap_qty': lot_data['qty_done'],
                'location_id': location_id.id,
            }
            if lot_data.get('uom'):
                data['product_uom_id'] = self._get_uom_id(lot_data['uom']).id
            else:
                data['product_uom_id'] = product_id.uom_id.id
            if post_data.get('date'):
                data['date_expected'] = post_data['date']
            if self._has_lot(product_id):
                data['lot_id'] = self._get_lot_id(lot_name=lot_data['lot_no'], product_code=post_data['product_code']).id,
            datas.append(data)

        scrap_ids = env['stock.scrap'].create(datas)
        for scrap_id in scrap_ids:
            res = scrap_id.action_validate()
            if isinstance(res, dict):
                raise UserError(f'产品"{product_id.display_name}",批次号"{scrap_id.lot_id.name}"在'
                                f'"{scrap_id.location_id.name}"没有足够的产品,无法报废,请确认位置和批次等信息正确!')

    def _execute_internal_picking(self, post_data):
        """执行正常的内部移库"""
        env = http.request.custom_env.env
        self._check_necessary_data_fields(post_data, fields=['product_code', 'customer_id', 'src_warehouse', 'dest_warehouse'])
        for lot_data in post_data.get('lot_lines', []):
            self._check_necessary_data_fields(lot_data, fields=['lot_no', 'qty_done'])

        product_id = self._get_product_id(post_data.get('product_code'))
        location_id = self._get_warehouse_id(post_data['src_warehouse'])
        location_dest_id = self._get_warehouse_id(post_data['dest_warehouse'])
        data = {
            'location_id': location_id.id,
            'location_dest_id': location_dest_id.id,
            'picking_type_id': self._get_internal_picking_type().id,
            'note': post_data.get('notes', ''),
            'move_lines': [(0, 0, {'product_id': product_id.id,
                                   'location_id': location_id.id,
                                   'location_dest_id': location_dest_id.id,
                                   'name': product_id.display_name,
                                   'product_uom': product_id.uom_id.id,
                                   })]
        }
        if post_data.get('customer_id'):
            company_id = self._get_company_base_customerid(post_data['customer_id'])
            if company_id:
                data['company_id'] = company_id.id
        if post_data.get('date'):
            data['scheduled_date'] = post_data['date']
            data['move_lines'][0][2]['date'] = post_data['date']

        picking_id = env['stock.picking'].create(data)
        move_id = picking_id.move_lines

        move_line_ids = move_id.move_line_ids
        move_lines = {'move_line_ids': []}

        for i in range(len(post_data['lot_lines']) - len(move_line_ids)):
            move_line_ids |= env['stock.move.line'].create(move_id._prepare_move_line_vals())

        for lot_data, move_line in zip(post_data['lot_lines'], move_line_ids):
            data = {'qty_done': lot_data['qty_done']}
            if self._has_lot(product_id):
                data['lot_id'] = self._get_lot_id(lot_name=lot_data['lot_no'],
                                                  product_code=post_data['product_code']).id,

            if lot_data.get('uom'):
                data['product_uom_id'] = self._get_uom_id(lot_data['uom']).id

            move_line_datas = move_lines['move_line_ids']
            move_line_datas.append((1, move_line.id, data))
        move_id.write(move_lines)
        # picking_id.action_confirm()
        # picking_id.action_done()
        picking_id.button_validate()


    def _get_company_base_customerid(self, customer_id):
        """根据货主ID获取对应的公司"""
        env = http.request.custom_env.env
        company_id = env['res.company'].search(['|', ('partner_id.nd_customer_code', '=', customer_id),
                                           ('partner_id.nd_supplier_code', '=', customer_id)], limit=1)
        return company_id

    def _get_internal_picking_type(self):
        """返回内部调拨picking type"""
        env = http.request.custom_env.env
        internal_picking = env.ref('stock.picking_type_internal')
        if not internal_picking:
            internal_picking = env['stock.picking.type'].search([('code', '=', 'internal')], limit=1)
        if not internal_picking:
            raise UserError('没有找到"内部移动"的库存移动类型,无法创建内部移库单!')
        return internal_picking

    @http.route(['/api/update_lot/'], methods=['POST'], auth='public', csrf=False, type='json')
    @json_response("更新批次")
    def nd_update_lot(self, *args, **kws):
        """更新批次内容,能变更的有:批次号名称,生产和过期日期
        data: [{'product_code':'产品编码', 'origin_lot_no ': '原批次号', 'customer_id': '货主ID',
        'new_lot_no': '新批次号, 'new_production_date': '新生产日期',
        'state': '质量状态:1为合格,0为不合格', 'state_qty': '不合格/合格 数量', 'uom': '单位'
        'new_expire_date': '新过期日期'}]

        new_lot_no, state, state_qty互为关联
        """
        post = http.request.jsonrequest
        self.pre_process(post)
        env = http.request.custom_env.env
        post_datas = post['data']
        for post_data in post_datas:
            self._check_necessary_data_fields(post_data, fields=['product_code', 'origin_lot_no'])
            lot_id = env['stock.production.lot'].search([('product_id.default_code', '=', post_data['product_code']),
                                                        ('name', '=', post_data['origin_lot_no'])])
            new_lot_id = False
            if not lot_id:
                raise UserError(f'产品"{post_data["product_code"]}"下没有找到旧批次号"{post_data["origin_lot_no"]}"!')

            # 先转移数量到新的批次号上
            if post_data.get('new_lot_no') and post_data.get('new_lot_no') != post_data.get('origin_lot_no') and post_data.get('state_qty'):
                new_lot_id = self._transafer_lot_to_lot(lot_id, post_data.get('new_lot_no'), post_data.get('state_qty'))

            to_modify_lot_id = new_lot_id or lot_id

            data = {}
            if post_data.get('new_lot_no'):
                data['name'] = post_data['new_lot_no']
            if post_data.get('new_production_date'):
                data['nd_production_date'] = post_data['new_production_date']
            if post_data.get('new_expire_date'):
                new_expire_date = self._to_datetime(post_data['new_expire_date'])
                life_date, alert_date, removal_date, use_date = env['stock.move.line'].nd_get_expire_date_info(new_expire_date)
                data.update({
                    'life_date': life_date,
                    'alert_date': alert_date,
                    'removal_date': removal_date,
                    'use_date': use_date,
                })

            if data:
                to_modify_lot_id.write(data)

            if post_data.get('state', '') == '1':
                # TODO 针对没有批次的如何处理?
                self.move_lot_to_unqualified_location(to_modify_lot_id, post_data)
            else:
                pass

    # def _transafer_lot_to_lot(self, lot_id, new_lot_no, qty):
    #     """利用评库表将从lot_id上移动qty个到new_lot_no， FIXME 但是评库表会产生额外的凭证"""
    #     env = http.request.custom_env.env
    #     qty = self._to_number(qty, _float=True)
    #     new_lot_id = env['stock.production.lot'].search([('product_id', '=', lot_id.product_id.id),
    #                                                      ('name', '=', new_lot_no)], limit=1)
    #     if not new_lot_id:
    #         new_lot_id = env['stock.production.lot'].create({'product_id': lot_id.product_id.id,
    #                                                          'name': new_lot_no,
    #                                                          'nd_production_date': lot_id.nd_production_date,
    #                                                          'use_date': lot_id.use_date,
    #                                                          'removal_date': lot_id.removal_date,
    #                                                          'life_date': lot_id.life_date,
    #                                                          'alert_date': lot_id.alert_date})
    #     location_ids = env['stock.location'].search([('usage', '=', 'internal'),
    #                                                  ('is_consignment', '=', False),
    #                                                  ('unqualified_location', '=', False)])
    #     quants = env['stock.quant']
    #     for quant in lot_id.quant_ids:
    #         if quant.quantity > 0 and quant.location_id in location_ids:
    #             quants |= quant
    #
    #     if sum(quants.mapped('quantity')) >= qty:
    #         total = qty
    #         for quant in quants:
    #             if total > 0:
    #                 move_qty = min(total, quant.quantity)
    #                 inventory_id = env['stock.inventory'].create({
    #                         'name': f'{lot_id.name}转移{qty}个至{new_lot_no}',
    #                         'location_id': quant.location_id.id,
    #                         'filter': 'lot',
    #                         'lot_id': quant.lot_id.id})
    #                 inventory_id.action_start()
    #                 # 我想象不出有多条明细的情况
    #                 inventory_id.line_ids[0].product_qty -= move_qty
    #                 theoretical_qty = quant.product_id.get_theoretical_quantity(
    #                     quant.product_id.id,
    #                     quant.location_id.id,
    #                     lot_id=new_lot_id.id,
    #                     to_uom=quant.product_uom_id.id,
    #                 )
    #                 inventory_line = env['stock.inventory.line'].create({
    #                     'product_id': quant.product_id.id,
    #                     'product_uom_id': quant.product_uom_id.id,
    #                     'location_id': quant.location_id.id,
    #                     'prod_lot_id': new_lot_id.id,
    #                     'product_qty': theoretical_qty + move_qty,
    #                     'inventory_id': inventory_id.id
    #                 })
    #                 inventory_id.action_validate()
    #                 total -= move_qty
    #     else:
    #         raise UserError(f'批次{lot_id.name}下的已有的数量{sum(quants.mapped("quantity"))}不足{qty}')
    #     return new_lot_id

    def _transafer_lot_to_lot(self, lot_id, new_lot_no, qty):
        """直接从底层修改quant和lot来从lot_id上移动qty个到new_lot_no"""
        env = http.request.custom_env.env
        qty = self._to_number(qty, _float=True)
        Quant = env['stock.quant']
        new_lot_id = env['stock.production.lot'].search([('product_id', '=', lot_id.product_id.id),
                                                         ('name', '=', new_lot_no)], limit=1)
        if not new_lot_id:
            new_lot_id = env['stock.production.lot'].create({'product_id': lot_id.product_id.id,
                                                             'name': new_lot_no,
                                                             'nd_production_date': lot_id.nd_production_date,
                                                             'use_date': lot_id.use_date,
                                                             'removal_date': lot_id.removal_date,
                                                             'life_date': lot_id.life_date,
                                                             'alert_date': lot_id.alert_date})
        location_ids = env['sale.order']._get_legal_locations()
        product_id = lot_id.product_id
        total_available_qty = 0     # 提示用，不参与逻辑
        for location_id in location_ids:
            # 原批次的数量信息
            available_qty = Quant._get_available_quantity(product_id,
                                                          location_id,
                                                          lot_id=lot_id)
            total_available_qty += available_qty
            if available_qty > 0 and qty > 0:
                move_qty = min(available_qty, qty)
                # 先减原批次数量， in_date参数可以指定入库时间
                Quant._update_available_quantity(product_id, location_id, -move_qty, lot_id=lot_id)
                # 再加新批次数量
                Quant._update_available_quantity(product_id, location_id, move_qty, lot_id=new_lot_id)
                qty -= move_qty
        if qty > 0:
            raise UserError(f'批次{lot_id.name}下的已有的数量{total_available_qty}不足 {qty}')
        return new_lot_id

    # def _update_lot_qty(self, product_id, old_lot_no, new_lot_no, qty, customer_id):
    #     """从old_lot_no里移动qty个产品至new_lot_no"""
    #     env = http.request.custom_env.env
    #     Lot = env['stock.production.lot']
    #     old_lot_id = Lot.search([('product_id', '=', product_id.id), ('name', '=', old_lot_no)], limit=1)
    #     new_lot_id = Lot.search([('product_id', '=', product_id.id), ('name', '=', new_lot_no)], limit=1)
    #     if not new_lot_id:
    #         new_lot_id = Lot.create({'name': new_lot_no, 'product_id': product_id.id})
    #
    #     location_ids = old_lot_id.quant_ids.filtered(lambda x: x.location_id.usage == 'internal' and x.quantity > 0).mapped('location_id')
    #
    #     data = {
    #         'name': f'{fields.Date.today()}-批次调整单',
    #         'location_id': location_ids[0].id,
    #         'filter': 'product',
    #         'product_id': product_id.id,
    #     }
    #
    #     company_id = self._get_company_base_customerid(customer_id)
    #     if company_id:
    #         data['company_id'] = company_id.id
    #
    #     inventory_id = env['stock.inventory'].create(data)
    #     inventory_id.action_start()
    #     inventory_lines = inventory_id.line_ids
    #     old_lot_inventory_line = inventory_lines.filtered(lambda x: x.prod_lot_id == old_lot_id)
    #     old_lot_inventory_line.product_qty = old_lot_inventory_line.theoretical_qty - qty
    #     new_lot_inventory_line = inventory_lines.filtered(lambda x: x.prod_lot_id == new_lot_id)
    #     if not new_lot_inventory_line:
    #         new_lot_inventory_line = env['stock.inventory.line'].create({'inventory_id': inventory_id.id,
    #                                                        'product_uom_id': product_id.uom_id.id,
    #                                                        'location_id': location_ids[0].id})
    #     new_lot_inventory_line.product_qty = qty
    #     inventory_id.action_validate()

    def move_lot_to_unqualified_location(self, lot_id, post_data):
        """将此批次移动至不合格品库位"""
        lot_id.ensure_one()
        env = http.request.custom_env.env
        unqualified_location = self.get_specific_location('unqualified')

        product_id = lot_id.product_id
        location_id = self.get_specific_location('qualified')
        data = {
            'location_id': location_id.id,
            'location_dest_id': unqualified_location.id,
            'picking_type_id': self._get_internal_picking_type().id,
            # 'note': post_data.get('notes', ''),
            'move_lines': [(0, 0, {'product_id': product_id.id,
                                   'location_id': location_id.id,
                                   'location_dest_id': unqualified_location.id,
                                   'name': product_id.display_name,
                                   'product_uom': product_id.uom_id.id,
                                   })]
        }
        if post_data.get('customer_id'):
            company_id = self._get_company_base_customerid(post_data['customer_id'])
            if company_id:
                data['company_id'] = company_id.id
        # if post_data.get('date'):
        #     data['scheduled_date'] = post_data['date']
        #     data['move_lines'][0][2]['date'] = post_data['date']

        picking_id = env['stock.picking'].create(data)
        move_id = picking_id.move_lines

        move_line_ids = move_id.move_line_ids
        move_lines = {'move_line_ids': []}

        # 需要找出那些需要移动的quant,一个批次可能分布在各个库位里,可能也只需要移动一部分产
        # 品到不合格品库位.TODO 对于移动一部分产品至不合格位置的,具体选择哪些库位里产品来移动,这里是随机的
        quant_ids = lot_id.quant_ids.filtered(lambda x: x.location_id.usage == 'internal' and
                                                        x.location_id.unqualified_location is False and
                                                        x.quantity > 0)
        if not quant_ids:
            raise UserError(f'产品"{post_data["product_code"]}"在批次"{post_data["origin_lot_no"]}"下已没有合格产品,无法转移至不合格品位置!')
        state_qty = self._to_number(post_data.get('state_qty', 0))
        temp_state_qty = state_qty
        need_move_quant_ids = env['stock.quant']
        for quant_id in quant_ids:
            if temp_state_qty > 0:
                need_move_quant_ids |= quant_id
                temp_state_qty -= quant_id.quantity

        for i in range(len(need_move_quant_ids) - len(move_line_ids)):
            move_line_ids |= env['stock.move.line'].create(move_id._prepare_move_line_vals())

        for quant, move_line in zip(need_move_quant_ids, move_line_ids):
            quantity_need_move = min(quant.quantity, state_qty)
            data = {'qty_done': quantity_need_move,
                    'lot_id': quant.lot_id.id,
                    'product_uom_id': quant.product_uom_id.id,
                    'location_id': quant.location_id.id,
                    'location_dest_id': unqualified_location.id,
                    }
            state_qty -= quantity_need_move
            move_line_datas = move_lines['move_line_ids']
            move_line_datas.append((1, move_line.id, data))
        move_id.write(move_lines)
        picking_id.button_validate()
        _logger.info(f'成功验证不合格品内部调拨单{picking_id}, 移动至{unqualified_location},参数为{post_data}')

    def move_lot_qualified_location(self, lot):
        """将批次移动至合格品库位"""
        raise NotImplementedError

    @staticmethod
    def get_specific_location(location_type):
        """获取特定类型的库位,
        type可能是qualified表示获取正常库位,
        type可能是unqualified表示不合格品库位"""
        env = http.request.custom_env.env
        if location_type == 'qualified':
            qualified_location = env['stock.location'].search([('usage', '=', 'internal'),
                                                               ('is_consignment', '=', False),
                                                               ('unqualified_location', '=', False)], limit=1)
            if not qualified_location:
                raise UserError('没有找到正常库位(非不合格品库位的内部位置),请配置一个!')
            return qualified_location
        elif location_type == 'unqualified':
            unqualified_location = env.ref('nd_lot.nd_unqualified_location')
            if not unqualified_location:
                unqualified_location = env['stock.location'].search([('usage', '=', 'internal'),
                                                                     ('is_consignment', '=', False),
                                                                     ('unqualified_location', '=', True)], limit=1)
            if not unqualified_location:
                raise UserError(f'没有找到不合格品库位,请先配置一个!')
            return unqualified_location



    """
    # 如何定位入库单？跟据H_EDI_09，也就是ERP里调拨单的ID，明细行也可以根据ID来
    # 出库单呢？没有ID
    # 入库单需要同步的字段：
        OrderNo：入库单号
        OrderType 单据类型
        CustomerID 货主代码
        WarehouseID 仓库ID
        Udf1  WMS单据号
        明细：
            SKU 产品编码
            ReceivedQty 实收数量
            ReceivedTime 收获时间
            
    # 出库单需要同步的字段：
        OrderNo 单据号
        OrderType 订单类型
        CustomerID 货主代码
        WarehouseID 库位
        DeliveryNo 运单号
        Weight 总重量
        CarrierId 承运商ID
        CarrierName 承运商名称
        明细行：
            OrderNo： 来源订单号
            LineNo 来源行号
            SKU 产品
            Weight 重量
            QtyShipped 实发数量
            ShippedTime 实发时间
            DeliveryNo 运单号
            Lotatt01 生产日期
            Lotatt02 失效日期
            
    # 创建评库单
    OrderNo 单号
    OrderType 单据类型（盘盈调整单、盘亏调整单）
    CustomerID 货主ID
    WarehouseID 仓库ID
    ResonCode 原因
    明细行
         SKU 产品   
        Qty 调整数量        
        Time 调整时间
        
    # 创建内部调拨
    OrderNo 单号
    OrderType 单据类型
    CustomerID 货主ID
    WarehouseID 仓库
    Udf1 批次增加或批次减少标记
    明细
        SKU 产品
        Qty 数量
        Time 时间
        Lotatt01 批次
        移动位置呢？？？
        
    # 库存移动
    OrderNo 单号
    OrderType 单据类型（转仓单）
    CustomerID 货主ID
    WarehouseID 仓库ID
    ResonCode 调整原因
    明细：
        SKU 产品
        Qty 数量
        Time 时间
        FMLOC 转出库位
        TOLOC 转入库位
        FMWH 转出仓库
        TOWH 转入仓库
    """


